9章 オブジェクト指向とオブジェクト指向プログラミング
https://gyazo.com/51ba590422cb6160a4960060509a9941
配列もオブジェクトも「コンテナ」と呼ばれる
配列とオブジェクトは以下の点で異なる
配列には値が含まれ、インデックスで要素にアクセスする。オブジェクトにはプロパティがあり、文字列あるいはシンボルを使って要素にアクセスする
配列の要素には順序がある。オブジェクトのプロパティには順序がない。
9.1 プロパティの列挙
コンテナに入っているものを一覧としてリストしたい場合はオブジェクトではなく配列を使ったほうがよい
オブジェクトもコンテナでありプロパティを列挙することもできる。
プロパティの列挙は順番がいつも同じとは限らない
9.1.1 for...in
オブジェクトのプロパティ処理に従来から使われている
for ... inを使って配列の要素を列挙することもできるが非推奨。
普通のforループを使うか、forEachを使ったほうがよい
code:js
const SYM = Symbol();
const o = { a: 1, b: 2, c: 3, SYM: 4 }; for(let prop in o) {
if(!o.hasOwnProperty(prop)) continue;
console.log(${prop}: ${o[prop]});
}
/* 実行結果 (キーがシンボルであるプロパティはリストされない)
a: 1
b: 2
c: 3
*/
[SYM]と書かれているのはシンボルをプロパティに含むオブジェクトをリテラル表記するため
hasOwnPropertyのチェックは省略できるが、hasOwnPropertyをを使うように習慣づけておいたほうがよい
オブジェクトは継承によってプロパティをもつ場合がある
独自のプロパティを列挙したい場合は一般にhasOwnPropertyのチェックが必要になる
9.1.2 Object.keys
Object.keys: 列挙可能な文字列のプロパティをすべて一つの配列に記憶できる
code:js
const SYM = Symbol();
const o = { a: 1, b: 2, c: 3, SYM: 4 }; const propArray = Object.keys(o);
console.log(propArray);
console.log("------");
propArray.forEach(prop => console.log(${prop}: ${o[prop]}));
/* 実行結果
------
a: 1
b: 2
c: 3
*/
Object.keysを使う場合はhasOwnPropertyでチェックする必要はない
filterと組み合わせる例
code:js
const o = { apple: 1, xochitl: 2, balloon: 3, guitar: 4, xylophone: 5, };
Object.keys(o)
.filter(prop => prop.match(/^x/))
.forEach(prop => console.log(${prop}: ${o[prop]}));
/* 実行結果
xochitl: 2
xylophone: 5
*/
9.2 オブジェクト指向プログラミング
オプジェクト思考プログラミング(OOP)
基本的なアイデアは単純で直感的です。「オブジェクト(もの)」は関連するデータと機能をまとめたもので、我々が世界を理解する方法を自然に反映したものです。
用語整理
クラス: オブジェクトの基になる雛形
インスタンス: 具体的なオブジェクト
メソッド: オブジェクトのもつ機能
クラスメソッド: クラス全体に関係するが特定のインスタンスには依存しないメソッド
コンストラクタ: インスタンス生成時に呼び出される機能
コンストラクタがオブジェクトのインスタンスを初期化する
OOPは階層化の枠組みも提供する
乗り物クラスは車よりは汎用的なクラスになる
乗り物は車のスーパークラス、車は乗り物のサブクラスになる
乗り物には車以外に船舶、バイク、飛行機…などのサブクラスをもつ
サブクラスはさらに下位のサブクラスを持つ場合がある(乗り物 > 船舶 > ボート)
9.2.1 クラスとインスタンス生成
ES2015からはclass構文でクラスを作成できるようになった。
インスタンス生成するにはnewを使う。
instanceof演算子: あるオブジェクトがあるクラスのオブジェクトであるかどうかがわかる
code:js
class Car {
constructor() {
}
}
const car1 = new Car();
const car2 = new Car();
console.log(car1 instanceof Car) // true
console.log(car2 instanceof Car) // true
console.log(car1 instanceof Array) // false
クラスの定義を書く際にはthisによって具体的に生成されたインスタンスを指す
9.2.2 アクセッサプロパティ
JavaScriptでは他のメソッドやプロパティに対するアクセス制御はできない
多くのオブジェクト指向言語にはある
アクセッサプロパティ(ダイナミックプロパティ)はこの弱点を補う
21章で詳しく説明
getあるいはsetに続いてメソッドを定義することで値の読み書きができるようになる
code:js
class Car {
constructor(make, model) {
this.make = make; /* メーカー */
this.model = model; /* モデル */
this._userGear = this._userGears0; }
get userGear() { return this._userGear; }
set userGear(value) {
if(this._userGears.indexOf(value) < 0) /* 例外処理(11章参照) */
throw new Error(ギア指定が正しくない:${value});
this._userGear = value;
}
shift(gear) { this.userGear = gear; }
}
const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
console.log(car1);
/* 実行結果
Car {
make: 'Tesla',
model: 'Model S',
_userGear: 'P' }
*/
console.log(car2);
/* 実行結果
Car {
make: 'Mazda',
model: '3i',
_userGear: 'P' }
*/
car1.shift('D');
car2.shift('R');
console.log(car1.userGear); // D
console.log(car2.userGear); // R
car1.userGear = "X"; // Error: ギア指定が正しくない:X
データの保護の観点から見ると、_userGearを直接変更することはできてしまう
_をつけることで本来すべきでないところでやっていればそのことがわかりやすくなる
もっと厳しくプライバシーを管理してたい場合はスコープによって保護されたウィークマップ(WeakMap)を使うことができる。(10章参照)
完全に外から操作できなくする例
code:js
const Car = (function() {
const carProps = new WeakMap(); /* 10章参照 */
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
carProps.set(this, { userGear: this._userGears0 }); }
get userGear() { return carProps.get(this).userGear; }
set userGear(value) {
if(this._userGears.indexOf(value) < 0)
throw new Error(ギア指定が正しくない:${value});
carProps.get(this).userGear = value;
}
shift(gear) { this.userGear = gear; } /* shiftの定義 */
} /* class Carの終わり */
return Car;
})();
const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
console.log(car1);
/* 実行結果
Car {
make: 'Tesla',
model: 'Model S',
userGear: 'P' }
*/
console.log(car2);
/* 実行結果
Car {
make: 'Mazda',
model: '3i',
userGear: 'P' }
*/
car1.shift('D');
car2.shift('R');
console.log(car1.userGear); // D
console.log(car2.userGear); // R
car1.userGear = "N";
console.log(car1.userGear); // N
car1.userGear = "X"; // Error: ギア指定が正しくない:X
IIEF(7章 スコープ)を使ってWeakMapをクロージャに隠している もう一つの例はプロパティ名としてシンボルを使うこと(21章)
偶発的な利用も避けることができる
クラス内のシンボルプロパティにはアクセスすることができてしまう
9.2.3 クラスは関数
ES2015以前ではクラスを生成するにはコンストラクタの役目をする関数を作っていた
内部的にはクラスの本質は変わっていない
classは糖衣構文
classは単なる関数
9.2.4 プロトタイプ
あるクラスのインスタンスに対して使えるメソッドを参照するとき、プロトタイプメソッドを参照している
プロトタイプメソッドを表すのに#を使うことがある
すべての関数はprototypeと呼ばれる特別なプロパティを持っている
オブジェクトのコンストラクタである関数では重要な役目をする
慣例によってオブジェクトのコンストラクタの名前は大文字で始める
prototypeプロパティはnewを使って新しいインスタンスを生成する場合に重要になる
新たに生成されたオブジェクトはコンストラクタのprototypeオブジェクトにアクセスできる
オブジェクトのインスタンスはこれをプロパティ__proto__に保存する
__で囲まれているプロパティはJavaScriptのベースの一部とみなされている
動的ディスパッチ
呼び出しと同じ意味合いで使われている
オブジェクトのプロパティ(メソッド)にアクセスしてそれが存在していないと、そのオブジェクトのプロトタイプを見て同じプロパティがないかチェックする
プロトタイプのデータプロパティを設定することは通常行わない
インスタンスに対してメソッドあるいはプロパティを定義するとプロトタイプでの定義よりも優先されることになる
code:js
// クラスの定義はexample/ch09/ex09-02-4/main.jsと同じ。
const car1 = new Car();
const car2 = new Car();
console.log(car1.shift === Car.prototype.shift); // true
console.log(car1.shift === car2.shift); // true
car1.shift('D');
// car1.shift('d'); // ここでこれを実行するとエラーになってしまう
console.log(car1.userGear); // D
// 新たにメソッドshiftを定義。小文字で指定されても大文字に変換する
car1.shift = function(gear) { this.userGear = gear.toUpperCase(); }
console.log(car1.shift === Car.prototype.shift); // false
console.log(car1.shift === car2.shift); // false
car1.shift('d'); // ここで実行してもエラーにはならない
console.log(car1.userGear); // D
// car2.shift('d'); // これを実行するとエラーになってしまう
9.2.5 静的メソッド
静的メソッド(クラスメソッド): インスタンスに付属しないメソッド
thisがクラスそのものに結び付けられるしかし、クラスの名前を使うほうがわかりやすい
クラスに関係する処理の実行に使われる
code:js
class Car {
static getNextVin() { // 車両番号を得る
return Car.nextVin++; // this.nextVin++でも動作するがクラス名を用いるほうがよい
}
constructor(make, model) {
this.make = make;
this.model = model;
this.vin = Car.getNextVin();
}
static areSimilar(car1, car2) { // メーカーとモデルが同じか
return car1.make===car2.make && car1.model===car2.model;
}
static areSame(car1, car2) { // 車両番号が同じか
return car1.vin===car2.vin;
}
}
Car.nextVin = 0;
const car1 = new Car("Tesla", "Model S");
const car2 = new Car("Mazda", "3i");
const car3 = new Car("Mazda", "3i");
console.log(car1.vin); // 0
console.log(car2.vin); // 1
console.log(car3.vin); // 2
console.log(Car.areSimilar(car1, car2)); // false
console.log(Car.areSimilar(car2, car3)); // true
console.log(Car.areSame(car2, car3)); // false
console.log(Car.areSame(car2, car2)); // true
9.2.6 継承
クラスのインスタンスを生成するとクラスのプロトタイプが持つ機能を継承(inherit)することになる
プロトタイプチェインが構築される
JavaScriptのシステムはリクエストが満たされるプロトタイプが見つかるまでプロトタイプチェインを遡る
一番上までさかのぼって見つからない場合はエラーになる
code:js
class Vehicle { /* 乗り物 */
constructor() {
this.passengers = []; /* 乗客 */
console.log("Vehicleが生成された");
}
addPassenger(p) { /* 乗客を追加 */
this.passengers.push(p);
}
}
class Car extends Vehicle { /* 車は乗り物のサブクラス */
constructor() {
super(); /* スーパークラスのコンストラクタを呼び出す */
console.log("Carが生成された");
}
deployAirbags() { /* エアバッグを作動させる */
console.log("バーンッ!");
}
}
extends: スーパークラスを指定する
CarはVehicleのサブクラスになった
super(): 特別な関数。スーパークラスのコンストラクタを呼び出す
サブクラス側では必ずこの関数を呼び出す必要がある
なければエラー
9.2.7 ポリモーフィズム
ポリモーフィズム: あるインスタンスをそのインスタンスが属するクラスのメンバーとして扱うだけでなく、スーパークラスのメンバーとしても扱う
JavaScriptでは変数に型を明示する必要がないため、オブジェクトを任意の場所で利用することができる
ある意味で究極のポリモーフィズム
JavaScriptのすべてのオブジェクトはルートクラスObjectのインスタンス
任意のオブジェクトoに対してo instanceof Objectはtrueになる
この性質はtoStringのようなすべてのオブジェクトが持つべきメソッドについて何かをするときに意味を持つことになる
9.2.8 プロパティの列挙(補足)
hasOwnPropertyの意味
オブジェクトobjとプロパティpropに対して、obj.hasOwnProperty(prop)はobjがプロパティpropをもつときにtrueを返す
propが定義されていないか、プロトタイプチェインで定義されている場合はfalseを返す
ES2015のclassを使えばデータプロパティはインスタンスに関して定義される
しかし、プロトタイプにプロパティを直接追加するのを防ぐことはできない
hasOwnPropertyで確認するほうが確実
code:js
class Super { /* スーパークラスの定義 */
constructor() {
this.name = 'Super';
this.isSuper = true;
}
}
Super.prototype.sneaky = '非推奨!'; /* こうすることは可能だが、非推奨 */
class Sub extends Super { /* サブクラスの定義 */
constructor() {
super();
this.name = 'Sub';
this.isSub = true;
}
}
const obj = new Sub(); /* サブクラスに属するオブジェクトをひとつ生成 */
for(let p in obj) {
console.log(${p}: ${obj[p]} +
(obj.hasOwnProperty(p) ? '' : ' (継承)')); /* 三項演算子 5章参照 */
}
/* 実行結果
name: Sub
isSuper: true
isSub: true
sneaky: 非推奨! (継承)
*/
プロパティname, isSuper, isSubはすべてインスタンスで定義されている
プロトタイプチェインでは定義されていない
プロパティsneakyはスーパークラスのprototypeに追加されている
Object.keysを使うことでこうした問題を避けることができる
Object.keys(obj)でobjに存在する列挙可能なプロパティの配列を返してくれる
9.2.9 文字列による表現
Objectで利用できるメソッドはすべてのオブジェクトで利用できる
toStringはデフォルトでは[object Object]を戻す
デバッグ時にオブジェクトの状態がわかって便利
code:js
class Car {
toString() {
return ${this.make} ${this.model} ${this.vin};
}
}
...
9.3 多重継承、ミックスイン、インターフェース
オブジェクト指向言語によっては多重継承と呼ばれる機構をサポートする
あるクラスが複数のスーパークラスの直下のサブクラスとなることができる
多重継承はコリジョン(衝突)の危険性をはらむことになる
多くの言語では多重継承を許していない
世の中の問題を考えた場合、多重継承が自然だと思われる場合も多々存在する
多重継承を許さない言語においてはインターフェースという機構を導入している
スーパークラスとしては親クラスからしか継承できないが、複数のインターフェースを持つことができる
JavaScriptはハイブリッド状態になっている
単一継承の言語であり、プロトタイプチェインは複数の親をさかのぼることはしない
多重継承の代わりにミックスイン(mixin)という概念が使われている
必要なときに機能がミックスされる
code:js
class Car {
constructor() {
}
}
class InsurancePolicy { /* 保険契約 */
}
function makeInsurable(o) {
o.addInsurancePolicy = function(p) { this.insurancePolicy = p; }
o.getInsurancePolicy = function() { return this.insurancePolicy; }
o.isInsured = function() { return !!this.insurancePolicy; }
}
makeInsurable(Car)とするとうまくいかない
code:js
const car1 = new Car();
makeInsurable(car1);
console.log(car1.isInsured()); // false
car1.addInsurancePolicy(new InsurancePolicy()); /* うまく行く */
console.log(car1.isInsured()); // true
これはうまく動作するが、車を生成するたびに呼び出さなくてはならない
code:js
makeInsurable(Car.prototype);
const car1 = new Car();
console.log(car1.isInsured()); // false
car1.addInsurancePolicy(new InsurancePolicy());
console.log(car1.isInsured()); // true
const car2 = new Car();
console.log(car2.isInsured()); // false
car2.addInsurancePolicy(new InsurancePolicy());
console.log(car2.isInsured()); // true
これで新しく作ったメソッドがいつもCarの一部であるかのごとく動作する
内部的にはこのメソッドはクラスCarの一部
この2つのクラスは個別に保守できる
ミックスインは衝突の問題をなくすというわけではない
保険をかけられるオブジェクトを識別するためにはinstanceofを使うことができない
ダックタイピングしかできない
シンボルを使うとこの問題を改善できる
code:js
const ADD_POLICY = Symbol();
const GET_POLICY = Symbol();
const IS_INSURED = Symbol();
const _POLICY = Symbol();
function makeInsurable(o) {
}
makeInsurable(Car.prototype);
const car1 = new Car();
const car2 = new Car();
シンボルはユニークなのでミックスインは既存のCarの機能を邪魔することはない
中間的なアプローチとしてはメソッドに対して普通の文字列を使い、データプロパティに対してはシンボルを用いる方法が考えられる
9.4 まとめ
オブジェクト指向は現実世界の問題に対してデバッグや修正、保守が簡単になるような組織化やカプセル化を助長する働きがある